package org.apereo.cas.adaptors.jdbc;

import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Map;

import javax.security.auth.login.AccountNotFoundException;
import javax.security.auth.login.FailedLoginException;
import javax.sql.DataSource;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.crypto.hash.ConfigurableHashService;
import org.apache.shiro.crypto.hash.DefaultHashService;
import org.apache.shiro.crypto.hash.HashRequest;
import org.apache.shiro.util.ByteSource;
import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult;
import org.apereo.cas.authentication.PreventedException;
import org.apereo.cas.authentication.UsernamePasswordCredential;
import org.apereo.cas.authentication.exceptions.AccountDisabledException;
import org.apereo.cas.authentication.exceptions.AccountPasswordMustChangeException;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.ServicesManager;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;

import com.gitee.linzl.secure.RSAEncryptUtil;

import lombok.extern.slf4j.Slf4j;

/**
 * 会员账号/手机号/邮箱登录,SHA加密算法+盐
 * 
 * 参考QueryAndEncodeDatabaseAuthenticationHandler修改，只是密码在页面已经加密传输了，需要在下方进行解密码
 * 
 * 加密方式 ：密码+固定盐+随机盐
 * 
 * @author linzl
 */
@Slf4j
public class QueryAndEncodeDatabaseAuthenticationHandlerExt extends AbstractJdbcUsernamePasswordAuthenticationHandler {
	public QueryAndEncodeDatabaseAuthenticationHandlerExt(String name, ServicesManager servicesManager,
			PrincipalFactory principalFactory, Integer order, DataSource dataSource) {
		super(name, servicesManager, principalFactory, order, dataSource);
	}

	/**
	 * The Algorithm name.
	 */
	protected String algorithmName;

	/**
	 * The Sql statement to execute.
	 */
	protected String sql;

	/**
	 * The Password field name.
	 */
	protected String passwordFieldName = "password";

	/**
	 * The Salt field name.
	 */
	protected String saltFieldName = "salt";
	/**
	 * The Expired field name.
	 */
	protected String expiredFieldName;

	protected String disabledFieldName;
	/**
	 * The Number of iterations field name.
	 */
	protected String numberOfIterationsFieldName;

	/**
	 * The number of iterations. Defaults to 0.
	 */
	protected long numberOfIterations;

	/**
	 * The static/private salt.
	 */
	protected String staticSalt;

	public QueryAndEncodeDatabaseAuthenticationHandlerExt(final String name, final ServicesManager servicesManager,
			final PrincipalFactory principalFactory, final Integer order, final DataSource dataSource,
			final String algorithmName, final String sql, final String passwordFieldName, final String saltFieldName,
			final String expiredFieldName, final String disabledFieldName, final String numberOfIterationsFieldName,
			final long numberOfIterations, final String staticSalt) {
		super(name, servicesManager, principalFactory, order, dataSource);
		this.algorithmName = algorithmName;
		this.sql = sql;
		this.passwordFieldName = passwordFieldName;
		this.saltFieldName = saltFieldName;
		this.expiredFieldName = expiredFieldName;
		this.disabledFieldName = disabledFieldName;
		this.numberOfIterationsFieldName = numberOfIterationsFieldName;
		this.numberOfIterations = numberOfIterations;
		this.staticSalt = staticSalt;
	}

	@Override
	protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(
			final UsernamePasswordCredential transformedCredential, final String originalPassword)
			throws GeneralSecurityException, PreventedException {
		if (StringUtils.isBlank(this.sql) || StringUtils.isBlank(this.algorithmName) || getJdbcTemplate() == null) {
			throw new GeneralSecurityException("Authentication handler is not configured correctly");
		}
		log.debug("进入自定义验证密码阶段:QueryAndEncodeDatabaseAuthenticationHandlerExt");
		// 可以通过转换成本身应用需要的用户名
		final String username = getPrincipalNameTransformer().transform(transformedCredential.getUsername());
		// 前端传入的密码通过rsa加密传输,需要先解密。TODO 密钥放缓存中
		String encodedPsw = RSAEncryptUtil.decrypt(originalPassword);
		// String encodedPsw = transformedCredential.getPassword();
		try {
			// add 会员账号/手机号/邮箱登录
			final Map<String, Object> values = getJdbcTemplate().queryForMap(this.sql, username, username, username);

			// final String digestedPassword = digestEncodedPassword(encodedPsw, values);
			// if (!values.get(this.passwordFieldName).equals(digestedPassword)) {
			// throw new FailedLoginException("Password does not match value on record.");
			// }
			if (!this.getPasswordEncoder().matches(encodedPsw, (String) values.get(this.passwordFieldName))) {
				throw new FailedLoginException("Password does not match value on record.");
			}
			// 配置为plainTextPasswordEncoder不加密
			// 密码定期过期，提醒用户修改密码
			if (StringUtils.isNotBlank(this.expiredFieldName) && values.containsKey(this.expiredFieldName)) {
				final String dbExpired = values.get(this.expiredFieldName).toString();
				if (BooleanUtils.toBoolean(dbExpired) || "1".equals(dbExpired)) {
					throw new AccountPasswordMustChangeException("Password has expired");
				}
			}

			// 用户被禁用，提醒用户去找管理员
			if (StringUtils.isNotBlank(this.disabledFieldName) && values.containsKey(this.disabledFieldName)) {
				final String dbDisabled = values.get(this.disabledFieldName).toString();
				if (BooleanUtils.toBoolean(dbDisabled) || "1".equals(dbDisabled)) {
					throw new AccountDisabledException("Account has been disabled");
				}
			}

			return createHandlerResult(transformedCredential, this.principalFactory.createPrincipal(username),
					new ArrayList<>(0));
		} catch (final IncorrectResultSizeDataAccessException e) {
			if (e.getActualSize() == 0) {
				throw new AccountNotFoundException(username + " not found with SQL query");
			} else {
				throw new FailedLoginException("Multiple records found for " + username);
			}
		} catch (final DataAccessException e) {
			throw new PreventedException("SQL exception while executing query for " + username, e);
		}

	}

	/**
	 * Digest encoded password.
	 *
	 * @param encodedPassword
	 *            the encoded password
	 * @param values
	 *            the values retrieved from database
	 * @return the digested password
	 */
	protected String digestEncodedPassword(final String encodedPassword, final Map<String, Object> values) {
		final ConfigurableHashService hashService = new DefaultHashService();

		if (StringUtils.isNotBlank(this.staticSalt)) {
			hashService.setPrivateSalt(ByteSource.Util.bytes(this.staticSalt));
		}
		hashService.setHashAlgorithmName(this.algorithmName);

		Long numOfIterations = this.numberOfIterations;
		if (values.containsKey(this.numberOfIterationsFieldName)) {
			final String longAsStr = values.get(this.numberOfIterationsFieldName).toString();
			numOfIterations = Long.valueOf(longAsStr);
		}

		hashService.setHashIterations(numOfIterations.intValue());
		if (!values.containsKey(this.saltFieldName)) {
			throw new RuntimeException("Specified field name for salt does not exist in the results");
		}

		final String dynaSalt = values.get(this.saltFieldName).toString();
		final HashRequest request = new HashRequest.Builder().setSalt(dynaSalt).setSource(encodedPassword).build();
		return hashService.computeHash(request).toHex();
	}
}
